package za.ac.wits.elen7045.persistence.dao;

import java.util.LinkedList;
import java.util.List;
import za.ac.wits.elen7045.persistence.Environment;
import za.ac.wits.elen7045.persistence.datasource.DataSource;
import za.ac.wits.elen7045.persistence.datasource.DataSourceFactory;
import za.ac.wits.elen7045.persistence.exception.DaoException;
import za.ac.wits.elen7045.persistence.exception.DataSourceProcessException;
import za.ac.wits.elen7045.persistence.model.Account;
import za.ac.wits.elen7045.persistence.model.Credential;
import za.ac.wits.elen7045.persistence.model.ServiceProvider;
import za.ac.wits.elen7045.persistence.model.ServiceProviderName;
import za.ac.wits.elen7045.persistence.model.User;
import za.ac.wits.elen7045.persistence.util.SQLQueries;

public class SqlAccountDAO implements SqlDao {

	private final int MAX_PLAIN_CREDENTIALS_ALLOWED = 5;
	private final int MAX_ENCRYPTED_CREDENTIALS_ALLOWED = 5;
	
	private DataSource<Account> accDSource;
	private DataSource<Credential> accCredDSource;
	
	/** 
	 * Constructor allows package access ONLY to force the use of the factory 
	 * */
	SqlAccountDAO(Environment env) {
		accDSource = DataSourceFactory.getDataSource(Account.class, env);
		accCredDSource = DataSourceFactory.getDataSource(Credential.class, env);
	}
	
	public Account getAccount(Account account) {
		try {
			accDSource.open();
			accCredDSource.open();
			/* get accounts from source */
			account = accDSource.retrieve(SQLQueries.getAccount(account)).iterator().next();
			if (account != null) {
				account.setCredentials(getAccountCredentials(account));
			}
			return account;
		} catch (Exception e) {
			throw new DaoException("Unable to load account from database!");
		} finally {
			accCredDSource.close();
			accDSource.close();
		}
	}
	
	public List<Account> getAccounts(User user) {
		try {
			accDSource.open();
			accCredDSource.open();
			String query = SQLQueries.getAccounts(user);
			List<Account> accounts = new LinkedList<Account>();
			/* get accounts from source */
			for (Account account : accDSource.retrieve(query)) {
				accounts.add(account);
			}
			/* Get account associated credentials for each account: 
			 * NOTE: 
			 * 		By making another call to source inside the above for-loop may result in 
			 * 		some database crashes and are therefore written as separated actions... 
			 */
			for (Account account : accounts) {
				account.setCredentials(getAccountCredentials(account));
			}
			return accounts;
		} catch (Exception e) {
			throw new DaoException("Unable to load accounts from database!");
		} finally {
			accCredDSource.close();
			accDSource.close();
		}
	}
	
	public List<Account> getAccounts(ServiceProvider serviceProvider) {
		return getAccounts(serviceProvider.getServiceProviderType());
	}
	
	public List<Account> getAccounts(ServiceProviderName serviceProviderType) {
		try {
			accDSource.open();
			accCredDSource.open();
			String query = SQLQueries.getAccounts(serviceProviderType);
			List<Account> accounts = new LinkedList<Account>();
			/* get accounts from source */
			for (Account account : accDSource.retrieve(query)) {
				accounts.add(account);
			}
			/* Get account associated credentials for each account: 
			 * NOTE: 
			 * 		By making another call to source inside the above for-loop may result in 
			 * 		some database crashes and are therefore written as separated actions... 
			 */
			for (Account account : accounts) {
				account.setCredentials(getAccountCredentials(account));
			}
			return accounts;
		} catch (Exception e) {
			throw new DaoException("Unable to load accounts from database!");
		} finally {
			accCredDSource.close();
			accDSource.close();
		}
	}
	
	public Credential getAccountCredential(Account account, String name) {
		try {
			accCredDSource.open();
			String query = SQLQueries.getAccountCredential(account, name);
			return accCredDSource.retrieve(query).iterator().next();
		} catch (Exception e) {
			throw new DataSourceProcessException("Unable to load account credentials from database!");
		} finally {
			accCredDSource.close();
		}
	}
	
	private List<Credential> getAccountCredentials(Account account) {
		try {
			accCredDSource.open();
			String query = SQLQueries.getAccountCredentials(account);
			List<Credential> credentials = new LinkedList<Credential>();
			for (Credential credential : accCredDSource.retrieve(query)) {
				credentials.add(credential);
			}
			return credentials;
		} catch (Exception e) {
			throw new DataSourceProcessException("Unable to load account credentials from database!");
		} finally {
			accCredDSource.close();
		}
	}

	private List<Credential> getAccountCredentials(Account account, boolean encrypted) {
		try {
			accCredDSource.open();
			String query = SQLQueries.getAccountCredentials(account);
			List<Credential> credentials = new LinkedList<Credential>();
			for (Credential credential : accCredDSource.retrieve(query)) {
				credentials.add(credential);
			}
			return credentials;
		} catch (Exception e) {
			throw new DataSourceProcessException("Unable to load account credentials from database!");
		} finally {
			accCredDSource.close();
		}
	}
	
	public boolean accountExist(Account account) {
		try {
			accDSource.open();
			String query = SQLQueries.accountExist(account);
			return accDSource.retrieve(query).iterator().hasNext();
		} catch (Exception e) {
			throw new DataSourceProcessException("Unable to verify account in database!");
		} 
		finally {
			accDSource.close();
		}
	}
	
	public boolean accountExist(User user, Account account) {
		try {
			accDSource.open();
			String query = SQLQueries.accountExist(user, account);
			return accDSource.retrieve(query).iterator().hasNext();
		} catch (Exception e) {
			throw new DataSourceProcessException("Unable to verify account in database!");
		} 
		finally {
			accDSource.close();
		}
	}
	
	public boolean accountCredentialExist(Account account, Credential credential) {
		try {
			accDSource.open();
			String query = SQLQueries.accountCredentialExist(account, credential);
			return accDSource.retrieve(query).iterator().hasNext();
		} catch (Exception e) {
			throw new DataSourceProcessException("Unable to verify account credential indatabase!");
		} 
		finally {
			accDSource.close();
		}
	}
	
	public void addAccount(User user, Account account) {
		if (account.getAccountOwnerName() == null) {
			account.setAccountOwnerName(user.getName());
		}
		if (accountExist(user, account)) {
			throw new DaoException("Account with account number '"+ account.getAccountNumber() +"' already exist!");
		}
		try {
			accDSource.open();
			/* Step 1 - add new account */
			accDSource.submit(SQLQueries.addAccount(account));
			/* Step 2 - get "updated" copy of account (with ID) */
			account = accDSource.retrieve(SQLQueries.getAccount(account)).iterator().next();
			/* Step 3 - add user-account reference in mapping table */
			accDSource.submit(SQLQueries.addUserAccountReference(user, account));
			accDSource.commit();
			/* Step 4 - add account credentials */
			List<Credential> credentials = account.getCredentials();
			if (credentials != null && !credentials.isEmpty()) {
				addCredentialsForAccount(account, credentials);
			}
		} catch (Exception e) {
			e.printStackTrace();
			throw new DaoException("Unable to add accounts to database!");
		} 
		finally {
			accDSource.close();
		}
	}
	
	public void addAccounts(User user, List<Account> accounts) {
		try {
			accDSource.open();
			for (Account account : accounts) {
				if (accountExist(user, account)) {
					throw new DaoException("Account with account number '"+ account.getAccountNumber() +"' already exist!");
				}
				/* Step 1 - add new account */
				accDSource.submit(SQLQueries.addAccount(account));
				/* Step 2 - get "updated" copy of account (with ID) */
				account = accDSource.retrieve(SQLQueries.getAccount(user, account)).iterator().next();
				/* Step 3 - add user-account reference in mapping table */
				accDSource.submit(SQLQueries.addUserAccountReference(user, account));
			}
			accDSource.commit();
		} catch (Exception e) {
			throw new DaoException("Unable to add accounts in database!");
		} 
		finally {
			accDSource.close();
		}
	}
	
	public void removeAccount(User user, Account account) {
		if (!accountExist(user, account)) {
			throw new DaoException("Account with account number '"+ account.getAccountNumber() +"' doesn't exist!");
		}
		try {
			accDSource.open();
			/* Step 1 - get "updated" copy of account */
			account = accDSource.retrieve(SQLQueries.getAccount(user, account)).iterator().next();
			/* Step 2 - remove account */
			accDSource.submit(SQLQueries.removeAccount(account));
			/* Step 3 - remove user-account reference in mapping table */
			accDSource.submit(SQLQueries.removeUserAccountReference(user, account));
			accDSource.commit();
			/* Step 4 - remove account credentials */
			List<Credential> credentials = account.getCredentials();
			if (credentials != null && !credentials.isEmpty()) {
				removeAccountCredentials(account, credentials);
			}
			/* Step 4 - remove account statements */
		} catch (Exception e) {
			throw new DaoException("Unable to remove account from database!");
		} 
		finally {
			accDSource.close();
		}
	}
	
	public void removeAccounts(User user, List<Account> accounts) {
		try {
			accDSource.open();
			for (Account account : accounts) {
				if (!accountExist(user, account)) {
					throw new DaoException("Account with account number '"+ account.getAccountNumber() +"' doesn't exist!");
				}
				/* Step 1 - get "updated" copy of account */
				account = accDSource.retrieve(SQLQueries.getAccount(user, account)).iterator().next();
				/* Step 2 - remove account */
				accDSource.submit(SQLQueries.removeAccount(account));
				/* Step 3 - remove user-account reference in mapping table */
				accDSource.submit(SQLQueries.removeUserAccountReference(user, account));
				accDSource.commit();
				/* Step 4 - remove account credentials */
				List<Credential> credentials = account.getCredentials();
				if (credentials != null && !credentials.isEmpty()) {
					addCredentialsForAccount(account, credentials);
				}
			}
		} catch (Exception e) {
			throw new DaoException("Unable to remove accounts from database!");
		} 
		finally {
			accDSource.close();
		}
	}
	
	public void addCredentialForAccount(Account account, Credential credential) {
		if (credential.getOwnerId() == 0) {
			credential.setOwnerId(account.getId());
		}
		if (accountCredentialExist(account, credential)) {
			throw new DaoException("Account credential with name '"+ credential.getName() +"' already exist!");
		} else if (!isMoreCredentialsAllowed(account, credential.isEncrypted())) {
			if (credential.isEncrypted()) {
				throw new DaoException("Unable to add account credential. The maximum of "+ MAX_ENCRYPTED_CREDENTIALS_ALLOWED +" encrypted credentials (per account) has been reached!");
			} else {
				throw new DaoException("Unable to add account credential. The maximum of "+ MAX_PLAIN_CREDENTIALS_ALLOWED +" unencrypted credentials (per account) has been reached!");
			}
		}
		try {
			accCredDSource.open();
			accCredDSource.submit(SQLQueries.addAccountCredential(credential));
			accCredDSource.commit();
		} catch (Exception e) {
			throw new DaoException("Unable to add account credentail to account in database!");
		} 
		finally {
			accCredDSource.close();
		}
	}
	
	public void addCredentialsForAccount(Account account, List<Credential> credentials) {
		try {
			accCredDSource.open();
			for (Credential credential : credentials) {
				if (accountCredentialExist(account, credential)) {
					throw new DaoException("Account credential with name '"+ credential.getName() +"' already exist!");
				} else if (!isMoreCredentialsAllowed(account, credential.isEncrypted())) {
					if (credential.isEncrypted()) {
						throw new DaoException("Unable to add account credential. The maximum of "+ MAX_ENCRYPTED_CREDENTIALS_ALLOWED +" encrypted credentials (per account) has been reached!");
					} else {
						throw new DaoException("Unable to add account credential. The maximum of "+ MAX_PLAIN_CREDENTIALS_ALLOWED +" unencrypted credentials (per account) has been reached!");
					}
				}
				accCredDSource.submit(SQLQueries.addAccountCredential(credential));
			}
			accCredDSource.commit();
		} catch (Exception e) {
			throw new DaoException("Unable to add account credentials to account in database!");
		} 
		finally {
			accCredDSource.close();
		}
	}
	
	public void removeAccountCredentials(Account account, List<Credential> credentials) {
		try {
			accCredDSource.open();
			for (Credential credential : credentials) {
				if (!accountCredentialExist(account, credential)) {
					throw new DaoException("Account credential with name '"+ credential.getName() +"' doesn't exist!");
				}
				accCredDSource.submit(SQLQueries.removeAccountCredential(credential));
			}
			accCredDSource.commit();
		} catch (Exception e) {
			throw new DaoException("Unable to remove account credentials from account in database!");
		} 
		finally {
			accCredDSource.close();
		}
	}
	
	public boolean isMoreCredentialsAllowed(Account account, boolean encrypted) {
		int maxCount = encrypted ? MAX_ENCRYPTED_CREDENTIALS_ALLOWED : MAX_PLAIN_CREDENTIALS_ALLOWED;
		return getAccountCredentials(account, encrypted).size() < maxCount;
	}
	
}
